// ##########################################################################
// #                                                                        #
// #                              CLOUDCOMPARE                              #
// #                                                                        #
// #  This program is free software; you can redistribute it and/or modify  #
// #  it under the terms of the GNU General Public License as published by  #
// #  the Free Software Foundation; version 2 or later of the License.      #
// #                                                                        #
// #  This program is distributed in the hope that it will be useful,       #
// #  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
// #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          #
// #  GNU General Public License for more details.                          #
// #                                                                        #
// #          COPYRIGHT: EDF R&D / TELECOM ParisTech (ENST-TSI)             #
// #                                                                        #
// ##########################################################################

#include "ccBoundingBoxEditorDlg.h"

// systems
#include <limits>

// Qt
#include <QClipboard>

// Box state at last dialog execution
static ccBBox s_lastBBox;

// Left 'point type' combo box indexes
static const int MinCornerIndex = 0;
static const int CenterIndex    = 1;
static const int MaxCornerIndex = 2;

// Helper
static void MakeSquare(ccBBox& box, int pivotType, int defaultDim = -1)
{
	assert(defaultDim < 3);
	assert(pivotType >= 0 && pivotType < 3);

	CCVector3 W = box.getDiagVec();
	if (W.x != W.y || W.x != W.z)
	{
		if (defaultDim < 0)
		{
			// we take the largest one!
			defaultDim = 0;
			if (W.u[1] > W.u[defaultDim])
				defaultDim = 1;
			if (W.u[2] > W.u[defaultDim])
				defaultDim = 2;
		}

		CCVector3 newW(W.u[defaultDim], W.u[defaultDim], W.u[defaultDim]);
		switch (pivotType)
		{
		case 0: // min corner
		{
			CCVector3 A = box.minCorner();
			box         = ccBBox(A, A + newW, box.isValid());
		}
		break;
		case 1: // center
		{
			CCVector3 C = box.getCenter();
			box         = ccBBox(C - newW / 2.0, C + newW / 2.0, box.isValid());
		}
		break;
		case 2: // max corner
		{
			CCVector3 B = box.maxCorner();
			box         = ccBBox(B - newW, B, box.isValid());
		}
		break;
		}
	}
}

ccBoundingBoxEditorDlg::ccBoundingBoxEditorDlg(bool showBoxAxes, bool showRasterGridImage, QWidget* parent /*=nullptr*/)
    : QDialog(parent, Qt::Tool)
    , Ui::BoundingBoxEditorDialog()
    , m_baseBoxIsMinimal(false)
    , m_showInclusionWarning(true)
{
	setupUi(this);

	oriGroupBox->setVisible(showBoxAxes);
	rasterGridImageLabel->setVisible(showRasterGridImage);
	resize(QSize(width(), computeBestDialogHeight(showBoxAxes, showRasterGridImage)));

	xDoubleSpinBox->setMinimum(-1.0e9);
	yDoubleSpinBox->setMinimum(-1.0e9);
	zDoubleSpinBox->setMinimum(-1.0e9);
	xDoubleSpinBox->setMaximum(1.0e9);
	yDoubleSpinBox->setMaximum(1.0e9);
	zDoubleSpinBox->setMaximum(1.0e9);

	dxDoubleSpinBox->setMinimum(0.0);
	dyDoubleSpinBox->setMinimum(0.0);
	dzDoubleSpinBox->setMinimum(0.0);
	dxDoubleSpinBox->setMaximum(1.0e9);
	dyDoubleSpinBox->setMaximum(1.0e9);
	dzDoubleSpinBox->setMaximum(1.0e9);

	connect(keepSquareCheckBox, &QCheckBox::toggled, this, &ccBoundingBoxEditorDlg::squareModeActivated);
	connect(okPushButton, &QPushButton::clicked, this, &ccBoundingBoxEditorDlg::saveBoxAndAccept);
	connect(cancelPushButton, &QPushButton::clicked, this, &ccBoundingBoxEditorDlg::cancel);
	connect(defaultPushButton, &QPushButton::clicked, this, &ccBoundingBoxEditorDlg::resetToDefault);
	connect(lastPushButton, &QPushButton::clicked, this, &ccBoundingBoxEditorDlg::resetToLast);
	connect(fromClipboardPushButton, &QPushButton::clicked, this, &ccBoundingBoxEditorDlg::fromClipboardClicked);
	connect(toClipboardPushButton, &QPushButton::clicked, this, &ccBoundingBoxEditorDlg::toClipboardClicked);

	connect(pointTypeComboBox, qOverload<int>(&QComboBox::currentIndexChanged), this, &ccBoundingBoxEditorDlg::reflectChanges);

	connect(xDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::updateCurrentBBox);
	connect(yDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::updateCurrentBBox);
	connect(zDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::updateCurrentBBox);

	connect(dxDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::updateXWidth);
	connect(dyDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::updateYWidth);
	connect(dzDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::updateZWidth);

	connect(xOriXDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(xOriYDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(xOriZDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(yOriXDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(yOriYDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(yOriZDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(zOriXDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(zOriYDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);
	connect(zOriZDoubleSpinBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ccBoundingBoxEditorDlg::onAxisValueChanged);

	defaultPushButton->setVisible(false);
	lastPushButton->setVisible(s_lastBBox.isValid());
	checkBaseInclusion();

	if (showRasterGridImage)
	{
		pointTypeComboBox->setCurrentIndex(MinCornerIndex);
	}
}

void ccBoundingBoxEditorDlg::setBox(const ccBBox& box)
{
	m_currentBBox = box;
	reflectChanges();
	checkBaseInclusion();
}

bool ccBoundingBoxEditorDlg::keepSquare() const
{
	return keepSquareCheckBox->isChecked();
}

void ccBoundingBoxEditorDlg::forceKeepSquare(bool state)
{
	if (state)
		keepSquareCheckBox->setChecked(true);
	keepSquareCheckBox->setDisabled(state);
}

void ccBoundingBoxEditorDlg::squareModeActivated(bool state)
{
	if (state)
	{
		MakeSquare(m_currentBBox, pointTypeComboBox->currentIndex());
		reflectChanges();
	}
}

void ccBoundingBoxEditorDlg::set2DMode(bool state, unsigned char dim)
{
	bool hideX = (state && dim == 0);
	bool hideY = (state && dim == 1);
	bool hideZ = (state && dim == 2);

	xDoubleSpinBox->setHidden(hideX);
	dxDoubleSpinBox->setHidden(hideX);
	xLabel->setHidden(hideX);

	yDoubleSpinBox->setHidden(hideY);
	dyDoubleSpinBox->setHidden(hideY);
	yLabel->setHidden(hideY);

	zDoubleSpinBox->setHidden(hideZ);
	dzDoubleSpinBox->setHidden(hideZ);
	zLabel->setHidden(hideZ);
}

void ccBoundingBoxEditorDlg::setBaseBBox(const ccBBox& box, bool isMinimal /*=true*/)
{
	// set new default one
	m_initBBox = m_baseBBox = box;
	m_baseBoxIsMinimal      = isMinimal;

	defaultPushButton->setVisible(m_baseBBox.isValid());

	resetToDefault();
}

void ccBoundingBoxEditorDlg::checkBaseInclusion()
{
	bool exclude = false;
	if (m_baseBBox.isValid())
	{
		exclude = !m_currentBBox.contains(m_baseBBox.minCorner()) || !m_currentBBox.contains(m_baseBBox.maxCorner());
	}

	warningLabel->setVisible(m_showInclusionWarning && exclude);
	okPushButton->setEnabled(!m_baseBoxIsMinimal || !exclude);
}

void ccBoundingBoxEditorDlg::resetToDefault()
{
	m_currentBBox = m_baseBBox;

	if (keepSquare())
		squareModeActivated(true); // will call reflectChanges
	else
		reflectChanges();

	checkBaseInclusion();
}

void ccBoundingBoxEditorDlg::resetToLast()
{
	m_currentBBox = s_lastBBox;

	if (keepSquare())
		squareModeActivated(true); // will call reflectChanges
	else
		reflectChanges();

	checkBaseInclusion();
}

void ccBoundingBoxEditorDlg::saveBoxAndAccept()
{
	if (oriGroupBox->isVisible())
	{
		CCVector3d X;
		CCVector3d Y;
		CCVector3d Z;
		getBoxAxes(X, Y, Z);
		X.normalize();
		Y.normalize();
		Z.normalize();
		if (X.norm2d() == 0
		    || Y.norm2d() == 0
		    || Z.norm2d() == 0)
		{
			ccLog::Error("Invalid axes definition: at least two vectors are colinear");
			return;
		}

		// if (	std::abs(X.dot(Y)) > 1.0e-6
		//	||	std::abs(Y.dot(Z)) > 1.0e-6
		//	||	std::abs(Z.dot(X)) > 1.0e-6 )
		//{
		//	ccLog::Error("Invalid axes definition: vectors must be orthogonal");
		//	return;
		// }
	}

	s_lastBBox = m_currentBBox;

	accept();
}

int ccBoundingBoxEditorDlg::exec()
{
	// backup current box
	m_initBBox = m_currentBBox;

	// call 'true' exec
	return QDialog::exec();
}

void ccBoundingBoxEditorDlg::cancel()
{
	// restore init. box
	setBox(m_initBBox);

	reject();
}

void ccBoundingBoxEditorDlg::updateXWidth(double value)
{
	updateCurrentBBox(value);
	if (keepSquare())
	{
		MakeSquare(m_currentBBox, pointTypeComboBox->currentIndex(), 0);
		reflectChanges();
		// base box (if valid) should always be included!
		if (m_baseBBox.isValid())
			checkBaseInclusion();
	}
}

void ccBoundingBoxEditorDlg::updateYWidth(double value)
{
	updateCurrentBBox(value);
	if (keepSquare())
	{
		MakeSquare(m_currentBBox, pointTypeComboBox->currentIndex(), 1);
		reflectChanges();
		// base box (if valid) should always be included!
		if (m_baseBBox.isValid())
			checkBaseInclusion();
	}
}

void ccBoundingBoxEditorDlg::updateZWidth(double value)
{
	updateCurrentBBox(value);
	if (keepSquare())
	{
		MakeSquare(m_currentBBox, pointTypeComboBox->currentIndex(), 2);
		reflectChanges();
		// base box (if valid) should always be included!
		if (m_baseBBox.isValid())
			checkBaseInclusion();
	}
}

void ccBoundingBoxEditorDlg::updateCurrentBBox(double dummy)
{
	CCVector3 A(static_cast<PointCoordinateType>(xDoubleSpinBox->value()),
	            static_cast<PointCoordinateType>(yDoubleSpinBox->value()),
	            static_cast<PointCoordinateType>(zDoubleSpinBox->value()));
	CCVector3 W(static_cast<PointCoordinateType>(dxDoubleSpinBox->value()),
	            static_cast<PointCoordinateType>(dyDoubleSpinBox->value()),
	            static_cast<PointCoordinateType>(dzDoubleSpinBox->value()));

	switch (pointTypeComboBox->currentIndex())
	{
	case 0: // A = min corner
		m_currentBBox = ccBBox(A, A + W, true);
		break;
	case 1: // A = center
		m_currentBBox = ccBBox(A - W / 2.0, A + W / 2.0, true);
		break;
	case 2: // A = max corner
		m_currentBBox = ccBBox(A - W, A, true);
		break;
	default:
		assert(false);
		return;
	}

	// base box (if valid) should always be included!
	if (m_baseBBox.isValid())
	{
		checkBaseInclusion();
	}
}

void ccBoundingBoxEditorDlg::reflectChanges(int dummy)
{
	// left column
	{
		xDoubleSpinBox->blockSignals(true);
		yDoubleSpinBox->blockSignals(true);
		zDoubleSpinBox->blockSignals(true);

		switch (pointTypeComboBox->currentIndex())
		{
		case MinCornerIndex: // A = min corner
		{
			const CCVector3& A = m_currentBBox.minCorner();
			xDoubleSpinBox->setValue(A.x);
			yDoubleSpinBox->setValue(A.y);
			zDoubleSpinBox->setValue(A.z);
		}
		break;
		case CenterIndex: // A = center
		{
			CCVector3 C = m_currentBBox.getCenter();
			xDoubleSpinBox->setValue(C.x);
			yDoubleSpinBox->setValue(C.y);
			zDoubleSpinBox->setValue(C.z);
		}
		break;
		case MaxCornerIndex: // A = max corner
		{
			const CCVector3& B = m_currentBBox.maxCorner();
			xDoubleSpinBox->setValue(B.x);
			yDoubleSpinBox->setValue(B.y);
			zDoubleSpinBox->setValue(B.z);
		}
		break;
		default:
			assert(false);
			return;
		}

		xDoubleSpinBox->blockSignals(false);
		yDoubleSpinBox->blockSignals(false);
		zDoubleSpinBox->blockSignals(false);
	}

	// right column
	{
		dxDoubleSpinBox->blockSignals(true);
		dyDoubleSpinBox->blockSignals(true);
		dzDoubleSpinBox->blockSignals(true);

		CCVector3 W = m_currentBBox.getDiagVec();
		// if 'square mode' is on, all width values should be the same!
		assert(!keepSquare() || std::abs(W.x - W.y) * 1.0e-6 < 1.0 && std::abs(W.x - W.z) * 1.0e-6 < 1.0);
		dxDoubleSpinBox->setValue(W.x);
		dyDoubleSpinBox->setValue(W.y);
		dzDoubleSpinBox->setValue(W.z);

		dxDoubleSpinBox->blockSignals(false);
		dyDoubleSpinBox->blockSignals(false);
		dzDoubleSpinBox->blockSignals(false);
	}
}

int ccBoundingBoxEditorDlg::computeBestDialogHeight(bool showBoxAxes, bool showRasterGridImage) const
{
	// we always keep space to display the grid
	int height     = gridFrame->height();
	int blockCount = 1;

	// we always keep space to display the warning label
	{
		height += warningLabel->height();
		++blockCount;
	}

	if (showBoxAxes)
	{
		height += oriGroupBox->height();
		++blockCount;
	}

	if (showRasterGridImage)
	{
		height += rasterGridImageLabel->height();
		++blockCount;
	}

	// we always keep space to display the buttons
	{
		height += buttonFrame->height();
		++blockCount;
	}

	if (layout())
	{
		height += layout()->contentsMargins().top() + layout()->spacing() * (blockCount - 1) + layout()->contentsMargins().bottom();
	}

	return height;
}

void ccBoundingBoxEditorDlg::setBoxAxes(const CCVector3& X, const CCVector3& Y, const CCVector3& Z)
{
	// if (xOriFrame->isEnabled())
	{
		xOriXDoubleSpinBox->setValue(X.x);
		xOriYDoubleSpinBox->setValue(X.y);
		xOriZDoubleSpinBox->setValue(X.z);
	}

	// if (yOriFrame->isEnabled())
	{
		yOriXDoubleSpinBox->setValue(Y.x);
		yOriYDoubleSpinBox->setValue(Y.y);
		yOriZDoubleSpinBox->setValue(Y.z);
	}

	// if (zOriFrame->isEnabled())
	{
		zOriXDoubleSpinBox->setValue(Z.x);
		zOriYDoubleSpinBox->setValue(Z.y);
		zOriZDoubleSpinBox->setValue(Z.z);
	}
}

void ccBoundingBoxEditorDlg::getBoxAxes(CCVector3d& X, CCVector3d& Y, CCVector3d& Z)
{
	X = CCVector3d(xOriXDoubleSpinBox->value(),
	               xOriYDoubleSpinBox->value(),
	               xOriZDoubleSpinBox->value());

	Y = CCVector3d(yOriXDoubleSpinBox->value(),
	               yOriYDoubleSpinBox->value(),
	               yOriZDoubleSpinBox->value());

	Z = CCVector3d(zOriXDoubleSpinBox->value(),
	               zOriYDoubleSpinBox->value(),
	               zOriZDoubleSpinBox->value());
}

void ccBoundingBoxEditorDlg::onAxisValueChanged(double)
{
	CCVector3d X;
	CCVector3d Y;
	CCVector3d Z;
	getBoxAxes(X, Y, Z);

	QDoubleSpinBox* vecSpinBoxes[3] = {nullptr, nullptr, nullptr};
	CCVector3d      N(0, 0, 0);
	if (oriXCheckBox->isChecked())
	{
		N               = Y.cross(Z);
		vecSpinBoxes[0] = xOriXDoubleSpinBox;
		vecSpinBoxes[1] = xOriYDoubleSpinBox;
		vecSpinBoxes[2] = xOriZDoubleSpinBox;
	}
	else if (oriYCheckBox->isChecked())
	{
		N               = Z.cross(X);
		vecSpinBoxes[0] = yOriXDoubleSpinBox;
		vecSpinBoxes[1] = yOriYDoubleSpinBox;
		vecSpinBoxes[2] = yOriZDoubleSpinBox;
	}
	else if (oriZCheckBox->isChecked())
	{
		N               = X.cross(Y);
		vecSpinBoxes[0] = zOriXDoubleSpinBox;
		vecSpinBoxes[1] = zOriYDoubleSpinBox;
		vecSpinBoxes[2] = zOriZDoubleSpinBox;
	}
	else
	{
		assert(false);
	}

	for (int i = 0; i < 3; ++i)
	{
		vecSpinBoxes[i]->blockSignals(true);
		vecSpinBoxes[i]->setValue(N.u[i]);
		vecSpinBoxes[i]->blockSignals(false);
	}
}

void ccBoundingBoxEditorDlg::fromClipboardClicked()
{
	QClipboard* clipboard = QApplication::clipboard();
	if (clipboard)
	{
		QString clipText = clipboard->text();
		if (!clipText.isEmpty())
		{
			bool       success = false;
			ccGLMatrix matrix  = ccGLMatrix::FromString(clipText, success);
			if (success)
			{
				// set center
				CCVector3 C     = m_currentBBox.getCenter();
				CCVector3 delta = matrix.getTranslationAsVec3D() - C;
				m_currentBBox += delta;
				reflectChanges();
				// change axes
				setBoxAxes(matrix.getColumnAsVec3D(0),
				           matrix.getColumnAsVec3D(1),
				           matrix.getColumnAsVec3D(2));
			}
			else
			{
				ccLog::Error("Failed to extract matrix from clipboard");
			}
		}
		else
		{
			ccLog::Error("Clipboard is empty");
		}
	}
}

void ccBoundingBoxEditorDlg::toClipboardClicked()
{
	QClipboard* clipboard = QApplication::clipboard();
	if (clipboard)
	{
		CCVector3 C = m_currentBBox.getCenter();

		CCVector3d X;
		CCVector3d Y;
		CCVector3d Z;
		getBoxAxes(X, Y, Z);

		ccGLMatrix matrix;
		matrix.setColumn(0, X.toPC());
		matrix.setColumn(1, Y.toPC());
		matrix.setColumn(2, Z.toPC());
		matrix.setTranslation(C);

		clipboard->setText(matrix.toString());
		ccLog::Print("Matrix saved to clipboard:");
		ccLog::Print(matrix.toString(12, ' ')); // full precision
	}
}
